/*
 * @(#)StockMIDlet.java	1.37 02/10/08 @(#)
 *
 * Copyright (c) 1999-2001 Sun Microsystems, Inc.  All rights reserved.
 * PROPRIETARY/CONFIDENTIAL
 * Use is subject to license terms.
 */

package example.stock;

import javax.microedition.rms.*;
import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
import javax.microedition.io.*;
import java.io.*;
import java.util.Timer;
import java.util.TimerTask;

/**
 * <p>The MIDlet application class that we'll run for the Stock Demo.</p>
 *
 * @author  Jeffrey Bacon
 */
public class StockMIDlet extends MIDlet {

    /**
     * Since there is no support in MIDP/CLDC for floating point numbers,
     * and all of the stock data comes in the form ##.### (usually) then we
     * have to find a way to store the decimals as well as the integer part
     * of the quote data.  Multiplying by the OFFSET will shift the decimal
     * place far enough over to make the number an integer which we can
     * store.  Every time we retrieve quote data from our
     * <code>RecordStore</code> we
     * must make sure that we divide by the OFFSET to correctly display and
     * use the value we actually want.
     */
    private static int OFFSET = 10000; // each 0 is one decimal place

    /**
     * The <code>MIDlet</code>'s display object
     */
    Display display = null;

    /**
     * The <code>Ticker</code> that scrolls along the top of the screen
     */
    private Ticker stockTicker = null;

    /**
     * A <code>List</code> of stocks
     */
    private List choose       = null;

    /**
     * The Stock Tracker menu
     */
    private List view         = null;

    /**
     * The Main menu
     */
    private List menu         = null;

    /**
     * The Alert menu
     */
    private List alertList    = null;

    /**
     * The Settings menu
     */
    private List settingsList = null;

    /**
     * The 'What If?' <code>Form</code> upon which we enter our query data
     */
    private Form whatif       = null;

    /**
     * The <code>Form</code> to display the different update intervals settings
     */
    private Form updatesForm  = null;

    /**
     * Used to input a stock symbol
     */
    private TextBox stockSymbolBox = null;

    /**
     * Used to enter the price at which the user wishes to be alerted to the
     * stocks's value
     */
    private TextBox alertPriceBox  = null;

    /**
     * The original price the user purchased the stock at, used on the What If?
     * <code>Form</code>
     */
    private TextField origPurchPriceField = null;

    /**
     * The number of shares the users wishes to sell, used on the What If?
     * <code>Form</code>
     */
    private TextField numSharesField      = null;

    /**
     * Back <code>Command</code>
     */
    private static final Command BACK_COMMAND     = 
	new Command("Back", Command.BACK, 0);

    /**
     * Main Menu <code>Command</code>
     */
    private static final Command MAIN_MENU_COMMAND = 
	new Command("Main", Command.SCREEN, 1);

    /**
     * Done <code>Command</code>
     */
    private static final Command DONE_COMMAND     = 
	new Command("Done", Command.OK,     2);

    /**
     * Set <code>Command</code>
     */
    private static final Command SET_COMMAND      = 
	new Command("Set", Command.OK,     4);

    /**
     * Exit <code>Command</code>
     */
    private static final Command EXIT_COMMAND     = 
	new Command("Exit", Command.STOP, 5);

    /**
     * Calc <code>Command</code>
     */
    private static final Command CALC_COMMAND     = 
	new Command("Calc", Command.OK,     6);

    /**
     * About <code>Command</code>
     */
    private static final Command ABOUT_COMMAND    = 
	new Command("About", Command.HELP,   1);

    /**
     * The radio buttons for the update interval time
     */
    private ChoiceGroup updatesChoices = null;

    /**
     * A textual reference to the current menu that is displayed
     * onscreen to
     * allow the <code>StockCommandListener</code> to decide what
     * action to perform upon
     * execution of a command
     */
    private String currentMenu = null;

    /**
     * A reference to the stock that has been chosen from a list of stocks
     * onscreen.  Using this we can extract the correct stock's data from
     * the <code>StockDatabase</code>
     */
    private String stockSymbol = null;

    /**
     * The reference to the <code>StockDatabase</code> that stores
     * the stock data
     */
    private StockDatabase stocks = null;

    /**
     * The reference to the <code>AlertDatabase</code> that stores the alerts
     */
    private AlertDatabase alerts = null;

    /**
     * The server from which the quotes are downloaded
     *
     * NOTE: Currently, only the quote.yahoo.com server is supported
     */
    private String quoteServerURL = "http://quote.yahoo.com/d/quotes.csv?s=";

    /**
     * The format parameter for the quote server to retrieve stock data
     *
     * NOTE: Currently, only this format is supported
     */
    private String quoteFormat = "&f=slc1wop";

    /**
     * The proxy server that must be negotiated
     *
     * NOTE: Proxy server is optional and a blank string indicates that
     *       no proxy should be used
     */
    // Initial default but read from settings file on subsequent runs
    private String proxyURL = null;

    /**
     * The <code>Timer</code> object that refreshes the stock
     * quotes periodically
     */
    private Timer stockRefresh = null;

    /**
     * The <code>TimerTask</code> that the <code>Timer</code>
     * performs periodically.
     * It refreshes the stock quotes
     */
    private StockRefreshTask stockRefreshTask = null;

    /**
     * How often are the stocks' data updated off of the server?
     */
    // Initial default but read from settings file on subsequent runs
    private int refresh_interval = 900000; // 1000 = 1 second

    /**
     * <p>Default constructor that is called by the application
     * manager to create a new instance of this <code>MIDlet</code> after
     * which the <code>MIDlet</code> enters the <code>Paused</code> state.</p>
     */
    public StockMIDlet() {}

    /**
     * <p>This method is invoked when the <code>MIDlet</code> 
     * is ready to run and
     * starts/resumes execution after being in the <code>Paused</code>
     * state. The
     * <code>MIDlet</code> acquires any resources it needs, 
     * enters the
     * <code>Active</code> state and begins to perform its service, 
     * which in this
     * case means it displays the main menu on the screen.</p>
     *
     * <p>The method proceeds like so:</p>
     *  <li> open the <code>StockDatabase</code> and the 
     * <code>AlertDatabase</code></li>
     *  <li> read the settings data from the settings 
     * <code>RecordStore</code></li>
     *  <li> create the string to be scrolled across the <code>Ticker</code> on
     *       the top of the screens and instantiate the Ticker object using that
     *       string.  That string will be constructed of the names and prices of
     *       the stocks in our database</li>
     *  <li> get and store the <code>MIDlet</code>'s
     *  <code>Display</code> object</li>
     *  <li> create and show the main menu</li>
     *  <li> instantiate the <code>TimerTask</code> and <code>Timer</code> and
     *       associate the two setting the refresh interval to the value of
     *       the refresh_interval variable</li>
     *
     * @throws <code>MIDletStateChangeException</code> is thrown if the
     *         <code>MIDlet</code> cannot start now but might be able to
     *         start at a later time.
     * @see javax.microedition.midlet.MIDlet
     */
    public void startApp() throws MIDletStateChangeException {
        synchronized (this) {
            display = Display.getDisplay(this);
            
            // Open the Stocks file
            stocks = new StockDatabase();
            try {
                stocks.open("Stocks");
            } catch (Exception e) {
                try {
                    stocks.cleanUp("Stocks");
                } catch (Exception e2) {}
            }
            
            // Open the Alerts file
            alerts = new AlertDatabase();
            try {
                alerts.open("Alerts");
            } catch (Exception e) {
                try {
                    alerts.cleanUp("Alerts");
                } catch (Exception e2) {}
            }
            
            // Open the Settings file
            try {
                RecordStore settings = 
                    RecordStore.openRecordStore("Settings", true);
                refresh_interval = 
                    Integer.valueOf(new String(settings.getRecord(1)))
                    .intValue();
                settings.closeRecordStore();
                
                // No settings file existed
            } catch (Exception e) {
                refresh_interval = 900000;
            }
            
            // Make the ticker
            stockTicker = new Ticker(makeTickerString());
            
            // Create all the menus and forms
            origPurchPriceField = 
                new TextField("Original Purchase Price:",     "", 5,  
                              TextField.NUMERIC);
            numSharesField      = 
                new TextField("Number Of Shares:",            "", 9,  
                              TextField.NUMERIC);
            
            menu = new List("Stock Menu", Choice.IMPLICIT);
            menu.append("Stock Tracker", null);
            menu.append("What If?", null);
            menu.append("Alerts", null);
            menu.append("Settings", null);
            menu.addCommand(EXIT_COMMAND);
            menu.addCommand(ABOUT_COMMAND);
            menu.setCommandListener(new StockCommandListener());
            menu.setTicker(stockTicker);
            
            whatif = new Form("What If?");
            whatif.setTicker(stockTicker);
            whatif.append(origPurchPriceField);
            whatif.append(numSharesField);
            whatif.addCommand(BACK_COMMAND);
            whatif.addCommand(CALC_COMMAND);
            whatif.setCommandListener(new StockCommandListener());
            
            alertList = new List("Alert Menu", Choice.IMPLICIT);
            alertList.setTicker(stockTicker);
            alertList.append("Add", null);
            alertList.append("Remove", null);
            alertList.addCommand(BACK_COMMAND);
            alertList.setCommandListener(new StockCommandListener());
            
            settingsList = new List("Settings", Choice.IMPLICIT);
            settingsList.setTicker(stockTicker);
            settingsList.append("Updates", null);
            settingsList.append("Add Stock", null);
            settingsList.append("Remove Stock", null);
            settingsList.addCommand(BACK_COMMAND);
            settingsList.setCommandListener(new StockCommandListener());
            
            alertPriceBox = new TextBox("Alert me when stock reaches:", 
                                        "", 9, TextField.NUMERIC);
            alertPriceBox.setTicker(stockTicker);
            alertPriceBox.addCommand(DONE_COMMAND);
            alertPriceBox.addCommand(BACK_COMMAND);
            alertPriceBox.setCommandListener(new StockCommandListener());
            
            updatesForm = new Form("Updates");
            updatesChoices = new ChoiceGroup("Update Interval:", 
                                             Choice.EXCLUSIVE);
            updatesChoices.append("Continuous", null); // will be 30 seconds
            updatesChoices.append("15 minutes", null); // default for JavaONE
            updatesChoices.append("30 minutes", null);
            updatesChoices.append("1 hour", null);
            updatesChoices.append("3 hours", null);
            switch (refresh_interval) {
                case 30000:    updatesChoices.setSelectedIndex(0, true);
                    break;
                case 1800000:  updatesChoices.setSelectedIndex(2, true);
                    break;
                case 3600000:  updatesChoices.setSelectedIndex(3, true);
                    break;
                case 10800000: updatesChoices.setSelectedIndex(4, true);
                    break;
                case 900000:
                default:       updatesChoices.setSelectedIndex(1, true);
                    break;
            }
            updatesForm.setTicker(stockTicker);
            updatesForm.append(updatesChoices);
            updatesForm.addCommand(BACK_COMMAND);
            updatesForm.addCommand(DONE_COMMAND);
            updatesForm.setCommandListener(new StockCommandListener());
            
            stockSymbolBox = new TextBox("Enter a Stock Symbol:",
                                         "", 5, TextField.ANY);
            stockSymbolBox.setTicker(stockTicker);
            stockSymbolBox.addCommand(DONE_COMMAND);
            stockSymbolBox.addCommand(BACK_COMMAND);
            stockSymbolBox.setCommandListener(new StockCommandListener());
            
            mainMenu();
            
            // Set up and start the timer to refresh the stock quotes
            stockRefreshTask = new StockRefreshTask();
            stockRefresh = new Timer();
            stockRefresh.schedule(stockRefreshTask, 0, refresh_interval);
            
            // FOR JavaONE -- ADD IN A COUPLE DEFAULT STOCKS
            /*
              addNewStock("MOT");
              addNewStock("SUNW");
              addNewStock("NOK");
              addNewStock("IBM");
              addNewStock("AOL");
              addNewStock("MSFT");
              addNewStock("GM");
              addNewStock("FORD");
              addNewStock("ORCL");
              addNewStock("SEEK");
              addNewStock("AT&T");
              addNewStock("LU");
              addNewStock("HON");
              addNewStock("CORL");
              addNewStock("NOR");
            */
        } // synchronized
    }

    /**
     * <p>This method is invoked by the application management software when
     * the <code>MIDlet</code> no longer needs to be active.  It is a stop
     * signal for the <code>MIDlet</code> upon which the <code>MIDlet</code>
     * should release any resources which can be re-acquired through the
     * <code>startApp<code> method which will be called upon re-activation of
     * the <code>MIDlet</code>. The <code>MIDlet</code> enters
     * the <code>Paused</code>
     * state upon completion of this method.</p>
     *
     * @see javax.microedition.midlet.MIDlet
     */
    public void pauseApp() {
        synchronized (this) {
            // free memory used by these objects
            display        = null;
            choose         = null;
            view           = null;
            menu           = null;
            alertList      = null;
            settingsList   = null;
            whatif         = null;
            updatesForm    = null;
            stockSymbolBox = null;
            alertPriceBox  = null;
            origPurchPriceField = null;
            numSharesField      = null;
            stockTicker         = null;
            stockRefresh.cancel();
            stockRefresh        = null;
            stockRefreshTask    = null;
            try {
                stocks.close();
                stocks = null;
                alerts.close();
                alerts = null;
            } catch (Exception e) {}
        } // synchronized
    }

    /**
     * <p>When the application management software has determined that the
     * <code>MIDlet</code> is no longer needed, or perhaps needs to make room
     * for a higher priority application in memory, is signals
     * the <code>MIDlet</code>
     * that it is a candidate to be destroyed by invoking the
     * <code>destroyApp(boolean)</code> method. In this case, we need to destroy
     * the <code>RecordEnumeration</code>s so that we don't waste their memory
     * and close the open <code>RecordStore</code>s.  If the 
     * <code>RecordStore</code>s
     * are empty, then we do not need them to be stored as 
     * they will be recreated
     * on the next invokation of the <code>MIDlet</code> so 
     * we should delete them.
     * At the end of our clean up, we must call <code>notifyDestroyed</code>
     * which will inform the application management software that we are done
     * cleaning up and have finished our execution and can 
     * now be safely terminated and
     * enters the <code>Destroyed</code> state.</p>
     *
     * @param unconditional If true when this method is called, 
     * the <code>MIDlet</code> must
     * cleanup and release all resources. If false the <code>MIDlet</code>
     * may throw <code>MIDletStateChangeException</code> to indicate it
     * does not want to be destroyed at this time.
     * @throws <code>MIDletStateChangeException</code>
     *         is thrown if the <code>MIDlet</code> wishes to continue to
     *         execute (Not enter the <code>Destroyed</code> state). This
     *         exception is ignored if <code>unconditional</code> is equal
     *         to true.
     * @see javax.microedition.midlet.MIDlet
     */
    public void destroyApp(boolean unconditional) 
        throws MIDletStateChangeException {

        // If there is no criteria that will keep us from terminating
        if (unconditional) {
            synchronized (this) {
                if (display == null) {
                    // If display == null, we are not initialized and
                    // we have nothing to destroy
                    return;
                }
                stockRefresh.cancel();
                try {
                    stocks.close();
                    alerts.close();
                    RecordStore settings = 
                        RecordStore.openRecordStore("Settings", true);
                    try {
                        settings.setRecord(1, 
                                           String.valueOf(refresh_interval)
                                           .getBytes(), 
                                           0, String.valueOf(refresh_interval)
                                           .length());
                        
                        // First time writing to the settings file
                    } catch (RecordStoreException rse) {
                        settings.addRecord(String.valueOf(refresh_interval)
                                           .getBytes(),
                                           0, String.valueOf(refresh_interval)
                                           .length());
                    }
                    settings.closeRecordStore();
                } catch (Exception e) {
                    // Ignore exception there is no place to report it
                }
                notifyDestroyed();
                
                // Something might make us not want to exit so check it 
                // here before terminating
            } // synchronized
        } else {
        }
    }

    /**
     * <p>Calculate the profits in a What If? scenario by the formula:</p>
     * <p><type>  Profit = (CurrentPrice - OriginalPurchasePrice)
     * * NumberOfShares</type></p>
     * <p>First we retrieve the current price of the stock. Then parse the
     * original purchase price that the user enters to format it to an
     * integer.  Next, retrieve the number of shares from the form that
     * the user filled in and then calculate the profits and display a nice
     * message (with the result) to the user onscreen.</p>
     */
    private void calc() {
        try {
            String s       = stocks.search(stockSymbol);
            int currPrice  = Stock.getPrice(s);
            int opp        = Stock.makeInt(origPurchPriceField.getString());
            int numShares  = 
		Integer.valueOf(numSharesField.getString()).intValue();
            int profit     = ((currPrice - opp) * numShares);

            Form answerForm = new Form(Stock.getName(s) + 
				       " " + Stock.getStringPrice(s));
            StringBuffer sb = 
		new StringBuffer().append("Net profit (loss) is ")
		    .append((profit >= 0) ? "$" : "($")
		    .append((profit >= 0) ? Stock.convert(profit) :
			    "-" + Stock.convert(profit))
		    .append((profit >= 0) ? "" : ")")
		    .append(" when selling ")
		    .append(String.valueOf(numShares))
		    .append(" shares at $")
		    .append(Stock.convert(currPrice))
		    .append(" per share.");
            answerForm.append(sb.toString());
            answerForm.addCommand(BACK_COMMAND);
            answerForm.addCommand(MAIN_MENU_COMMAND);
            answerForm.setCommandListener(new StockCommandListener());
            display.setCurrent(answerForm);
            currentMenu = "AnswerForm";
        }
        catch (Exception e) {
            error("Calculation Failed", 2000);
        }
    }

    /**
     * <p>Set an alert for the selected stock at the specified price</p>
     *
     * @param Sprice String representation of the price of the stock that
     *               the user would like an alert for
     */
    private void setAlert(String Sprice) {
        try {
            alerts.add((new StringBuffer()
                            .append(Stock.getName(stocks.search(stockSymbol)))
                            .append(';')
                            .append(Stock.makeInt(Sprice))).toString());
        } catch (Exception e) {
            error("Failed to add alert", 2000);
        }
    }

    /**
     * <p>Generate a string (which concatenates all of the stock names and
     * prices) which will be used for the <code>Ticker</code>.</p>
     *
     * @return The ticker string which concatenates all of the stock symbols
     *         and prices
     */
    private String makeTickerString() {

        // the ticker tape string
        StringBuffer tickerTape = new StringBuffer();

        try {
            RecordEnumeration re = stocks.enumerateRecords();
            while (re.hasNextElement()) {
                String theStock = new String(re.nextRecord());
                tickerTape.append(Stock.getName(theStock))
                          .append(" @ ")
                          .append(Stock.getStringPrice(theStock))
                          .append("   ");
            }
        } catch (Exception e) {
            return "Error Accessing Database";
        }
        return tickerTape.toString();
    }

    /**
     * <p>Display a message onscreen for a specified period of time</p>
     *
     * @param message The message to be displayed
     * @param time The delay before the message disappears
     */
    private void error(String message, int time) {
        if (!(display.getCurrent() instanceof Alert)) {
            Alert a = new Alert("Error", message, null, AlertType.ERROR);
            a.setTimeout(time);
            display.setCurrent(a, display.getCurrent());
        }
    }

    /**
     * <p>Check the alerts to see if any are registered for tkrSymbol at the
     * tkrSymbol's current price</p>
     *
     * @param tkrSymbol The name of the stock to check for alerts on
     */
    private void checkAlerts(String tkrSymbol) {
        try {
            int current_price = Stock.getPrice(stocks.search(tkrSymbol));
            RecordEnumeration re = alerts.enumerateRecords(tkrSymbol,
						       current_price);
            while (re.hasNextElement()) {
                String alert_string = new String(re.nextRecord());
                int my_price = 
		    Integer.valueOf(alert_string
				    .substring(alert_string.indexOf(';')+1,
					       alert_string.length()))
		    .intValue();
                Alert a = new Alert(tkrSymbol, "", null, AlertType.ALARM);
                StringBuffer sb = new StringBuffer()
                                  .append(tkrSymbol)
                                  .append(" has reached your price point of $")
                                  .append(Stock.convert(my_price))
                                  .append(" and currently is trading at $")
                                  .append(Stock.getStringPrice(stocks
						    .search(tkrSymbol)));
                a.setString(sb.toString());
                a.setTimeout(Alert.FOREVER);
                display.setCurrent(a);
                alerts.delete(alert_string);
            }
        } catch (RecordStoreNotOpenException rsno) {
        } catch (RecordStoreException rs) {
        }
    }

    /**
     * <p>Display the main menu of the program</p>
     */
    private void mainMenu() {
        display.setCurrent(menu);
        currentMenu = "Main";
    }

    /**
     * <p>Show the list of stocks to pick from and set the menu type to indicate
     * to the Listener what to do with the list choice</p>
     *
     * @param reload   Indicates whether the list should be reloaded
     * which should
     * only happen if it is possible that the stocks have changed
     * since it was last shown
     * @param menuType Which menu is this list representing
     * @param type     Type of Choice to display
     * @param prices   Indicates whether or not to show prices on the list
     */
    private void chooseStock(boolean reload, String menuType, int type,
			     boolean prices) {
        if (reload) {
            choose = new List("Choose Stocks", type);
            choose.setTicker(stockTicker);
            choose.addCommand(BACK_COMMAND);
            if (menuType.equals("RemoveStock")) {
                choose.addCommand(DONE_COMMAND);
            } else if (menuType.equals("WhatChoose") ||
                       menuType.equals("AddAlert")) {
                choose.addCommand(MAIN_MENU_COMMAND);
            }
            choose.setCommandListener(new StockCommandListener());

            try {
                RecordEnumeration re = stocks.enumerateRecords();
                while (re.hasNextElement()) {
                    String theStock = new String(re.nextRecord());
                    if (prices) {
                        choose.append(Stock.getName(theStock)
				      + " @ "
				      + Stock.getStringPrice(theStock), null);
                    } else {
                        choose.append(Stock.getName(theStock), null);
                    }
                }
            } catch (RecordStoreNotOpenException rsno) {
            } catch (RecordStoreException rs) {
            }
        }
        display.setCurrent(choose);
        currentMenu = menuType;
    }

    /**
     * <p>Display the stock selected on the View menu with all its
     * attributes shown (ie. Name, Price, etc.)</p>
     *
     * @param tkrSymbol The name of the stock to be deleted
     */
    private void displayStock(String tkrSymbol) {
        try {
            String theStock = stocks.search(tkrSymbol);
            Form stockInfo = new Form(Stock.getName(theStock));
            stockInfo.setTicker(stockTicker);
            StringBuffer sb = new StringBuffer()
                              .append("Last Trade:\n         ")
                              .append(Stock.getTime(theStock))
                              .append("\n         ")
                              .append(Stock.getStringPrice(theStock))
                              .append("\nChange:  ")
                              .append(Stock.getStringChange(theStock))
                              .append("\nHigh:  ")
                              .append(Stock.getStringHigh(theStock))
                              .append("\nLow:  ")
                              .append(Stock.getStringLow(theStock))
                              .append("\nOpen:  ")
                              .append(Stock.getStringOpen(theStock))
                              .append("\nPrev:  ")
                              .append(Stock.getStringPrevious(theStock));
            stockInfo.append(sb.toString());
            stockInfo.addCommand(BACK_COMMAND);
            stockInfo.addCommand(MAIN_MENU_COMMAND);
            stockInfo.setCommandListener(new StockCommandListener());
            display.setCurrent(stockInfo);
            currentMenu = "stockInfo";
        } catch (RecordStoreNotOpenException rsno) {
            error("Could not display stock.  ", 2000);
        } catch (RecordStoreException rs) {
            error("Could not display stock.  ", 2000);
        } catch (NullPointerException npe) {
            error("Could not display stock.  ", 2000);
        }
    }

    /**
     * <p>Show the What If? form to investigate a hypothetical stock deal</p>
     *
     * @param tkrSymbol The name of the stock to perform the query with
     */
    private void whatIfForm(String tkrSymbol) {
        display.setCurrent(whatif);
        currentMenu = "WhatIfForm";
        stockSymbol = tkrSymbol;
    }

    /**
     * <p>Show the alert management menu</p>
     *
     * @param showAlert Indicate whether we should show an alert to indicate
     *                  that we have just successfully added an alert
     */
    private void alertMenu(boolean showAlert) {
        display.setCurrent(alertList);
        currentMenu = "AlertMenu";
        if (showAlert) {
            Alert a = new Alert("", "\n\n\n   Saved!", null, null);
            a.setTimeout(2000);
            display.setCurrent(a, alertList);
        }
    }

    /**
     * <p>Show a list of all active alerts</p>
     */
    private void viewAlerts() {
        choose = new List("Current Alerts", Choice.MULTIPLE);
        choose.setTicker(stockTicker);
        try {

            // Get all the Alert records
            RecordEnumeration re = alerts.enumerateRecords("", 0);
            while (re.hasNextElement()) {
                String a = new String(re.nextRecord());
                String price = 
		    Stock.convert(Integer.valueOf(a.substring(a.indexOf(';')+1,
						      a.length())).intValue());
                choose.append(a.substring(0, a.indexOf(';')) + " @ $"
			      + price, null);
            }
        } catch (Exception e) {
            error("Error reading alerts", 2500);
        }
        choose.addCommand(BACK_COMMAND);
        choose.addCommand(DONE_COMMAND);
        choose.setCommandListener(new StockCommandListener());
        display.setCurrent(choose);
        currentMenu = "RemoveAlert";
    }

    /**
     * <p>Show the form to add an alert</p>
     *
     * @param tkrSymbol The ticker symbol of the stock we're adding an alert to
     */
    private void alertForm(String tkrSymbol) {
        display.setCurrent(alertPriceBox);
        currentMenu = "AlertForm";
        stockSymbol = tkrSymbol;
    }

    /**
     * <p>Remove the alert from our RecordStore referenced by index</p>
     *
     * @param choose_data A string with the symbol and price of the alert
     *                    to remove in it
     */
    private void removeAlert(String choose_data) {
        try {

            // Separate the symbol and price from the data
            String symbol = choose_data.substring(0,
						  choose_data.indexOf('@')-1);
            int sPrice = Stock.makeInt( 
                         choose_data.substring(choose_data.indexOf('@')+3,
                                                  choose_data.length()));
            System.out.println("Remove Alert: " + symbol + ";" + sPrice);
            // Remove the alert
            alerts.delete(symbol + ";" + sPrice);
        } catch (Exception e) {
            error("Failed to remove alert", 2000);
        }
    }

    /**
     * <p>Show the settings menu</p>
     *
     * @param showAlert Indicate whether we should show an alert to indicate
     *                  that we have just successfully saved changes to the
     *                  settings
     */
    private void settings(boolean showAlert) {
        display.setCurrent(settingsList);
        currentMenu = "Settings";
        if (showAlert) {
            Alert a = new Alert("", "\n\n\n   Saved!", null, null);
            a.setTimeout(1500);
            display.setCurrent(a, settingsList);
        }

    }

    /**
     * <p>Show the updates choices</p>
     */
    private void updates() {
        display.setCurrent(updatesForm);
        currentMenu = "Updates";
    }

    /**
     * <p>Show the screen to add a stock</p>
     */
    private void addStock() {
        stockSymbolBox.setString("");
        display.setCurrent(stockSymbolBox);
        currentMenu = "AddStock";
    }

    /**
     * <p>Add the stock to the database</p>
     *  <li> first contact the quote server to get the stock quote</li>
     *  <li> if the stock doesn't not exist, alert the user and return to
     *       the add screen</li>
     *  <li> if the stock exists then add it to our list</li>
     *  <li> if the addition of the stock was successful, return true
     *       otherwise, return false</li>
     * <BR>
     * <p>This is the format returned by the quote.yahoo.com server in
     * the format
     * specified in the instance variable:</p>
     * <pre>
     * NAME    TIME         PRICE        CHANGE   LOW       HIGH    OPEN PREV
     * "SUNW","11:24AM - <b>79.0625</b>",-3.0625,"26.9375 - 106.75",80.5,82.125
     * </pre>
     * <BR>
     * <p>This is what is returned if the stock is not found:</p>
     * <pre>
     * "NAME" ,"N/A - <b>0.00</b>"       ,N/A    ,"N/A - N/A"       ,N/A ,N/A
     * </pre>
     *
     * @return whether or not the addition of the stock was successful
     * @param tkrSymbol The ticker symbol of the stock to add
     */
    private boolean addNewStock(String tkrSymbol) {
        try {

            // When stocks.search() returns null, the stock doesn't yet exist
            if (stocks.search(tkrSymbol) == null) {
                try {
                    stocks.add(getStockQuote(tkrSymbol));
                    stockTicker.setString(makeTickerString());
                } catch (RecordStoreFullException rsf) {
                    error("Database is full.", 2000);
                    return false;
                } catch (RecordStoreException rs) {
                    error("Failed to add " + tkrSymbol, 2000);
                    return false;
                } catch (IOException ioe) {
                    error("Failed to download stock quote for \"" + 
			  tkrSymbol + "\"", 2000);
                    return false;
                } catch (NumberFormatException nfe) {
                    error("\"" + tkrSymbol + 
			  "\" not found on server, or invalid data "
			  + "received from server",
			  2000);
                    return false;
                }

            // The stock already exists so we'll just update it
            } else {
                try {
                    stocks.update(tkrSymbol,
				  getStockQuote(tkrSymbol).getBytes());
                    stockTicker.setString(makeTickerString());
                } catch (RecordStoreFullException rsf) {
                    error("Database is full.", 2000);
                    return false;
                } catch (RecordStoreException rs) {
                    error("Failed to update " + tkrSymbol, 2000);
                    return false;
                } catch (IOException ioe) {
                    error("Failed to download stock quote for " 
			  + tkrSymbol, 2000);
                    return false;
                }
            }
        } catch (RecordStoreException rs) {
            error("Error accessing database.", 2000);
            return false;
        }
        return true;
    }

    /**
     * <p>This method actually contacts the server, downloads and returns
     * the stock quote.</p>
     *
     * NOTE: If PROXY support is added to HttpConnection that switch the code
     *       over to that as it will be pure MIDP instead of this hack
     *
     * @return the stock quote
     * @param tkrSymbol The Stock to be requested from the server
     * @throws <code>IOException</code> is thrown if there is a problem
     *         negotiating a connection with the server
     * @throws <code>NumberFormatException</code> is thrown if trashed data
     *         is received from the server (or the Stock could not be found)
     */
    private String getStockQuote(String tkrSymbol)
	throws IOException, NumberFormatException {
        String quoteURL = quoteServerURL + tkrSymbol + quoteFormat;

        StreamConnection c = (StreamConnection)
	    Connector.open(quoteURL, Connector.READ_WRITE);
        InputStream is = c.openInputStream();
        int ch;
        StringBuffer sb = new StringBuffer();
        while ((ch = is.read()) != -1) {
            sb.append((char)ch);
        }
        Stock.parse(sb.toString());
	is.close();
	c.close();
        return sb.toString();
    }

    /**
     * <p>Remove the stock from the <code>StockDatabase</code></p>
     *
     * @param tkrSymbol The ticker symbol of the stock to delete
     */
    private void deleteStock(String tkrSymbol) {
        try {
            stocks.delete(tkrSymbol);
            alerts.removeUselessAlerts(tkrSymbol);
            stockTicker.setString(makeTickerString());
        } catch (RecordStoreException rs) {
            error("Failed to delete " + tkrSymbol, 2000);
        }
    }

    /**
     * <p>Show the about box with copyright info and image</p>
     */
    private void about() {
        Runtime runtime = Runtime.getRuntime();
        /*
        StringBuffer props = new StringBuffer();
        props.append("\nSystem Properties\n");
        props.append("Used Memory = " 
		     + (runtime.totalMemory() - runtime.freeMemory()) + "\n");
        props.append("Garbage collecting . . .\n");

        runtime.gc();
        long free = runtime.freeMemory();
        long total = runtime.totalMemory();

        props.append("Used Memory = " + (total - free) + "\n");
        props.append("Free Memory = " + free + "\n");
        props.append("Total Memory = " + total + "\n");
        System.out.println(props.toString());
        */
        example.About.showAbout(display);
    }

    /**
     * <p>This class is the Listener for ALL of the events that
     * take place during
     * life span of the <code>MIDlet</code>.  It handles <code>Command</code>
     * events and list selections which are the only events that this
     * <code>MIDlet</code> will generate.  In order to determine what to do,
     * the Listener checks the name of the command and the currently displayed
     * menu/list and matches them up to execute the appropriate action.</p>
     * <BR>
     * NOTE: The parseCommandString(String) is only there because the getXXX
     *       methods are missing from the Command class.  When they are added,
     *       this method can be removed and the Command.getLabel() can be used
     */

    private class StockCommandListener implements CommandListener, Runnable {
        /** Current command to proccess. */
        private Command currentCommand;
        
        /** Displyable of the current command to proccess. */
        private Displayable currentDisplayable;
        
        /** The current command processing thread. */
        private Thread commandThread;

        /**
         * <p>The method to determine what action to take</p>
         *
         * @param c The <code>Command</code> object that has been activated
         * @param d The <code>Displayable</code> object that the command was
         *          associated with
         */
        public void commandAction(Command c, Displayable d) {
            synchronized (this) {
                if (commandThread != null) {
                    // process only one command at a time
                    return;
                }

                currentCommand = c;
                currentDisplayable = d;
                commandThread = new Thread(this);
                commandThread.start();
            }
        }

        /**
         * Perform the current command set by the method commandAction.
         */
        public void run() {
            String type = currentCommand.getLabel();

            // Main command executed, always show Main Menu regardless of
            // which screen is showing
            if (type.equals("Main")) {
                mainMenu();

                // Back command executed, check which screen is and move to
                // the previous one
            } else if (type.equals("Back")) {

                // Screens off the Main Menu
                if (currentMenu.equals("View")           ||
                    currentMenu.equals("WhatChoose") ||
                    currentMenu.equals("AlertMenu")  ||
                    currentMenu.equals("Settings")) {
                    mainMenu();

                    // Screens off the Settings menu
                } else if (currentMenu.equals("Add")     ||
                           currentMenu.equals("Updates") ||
                           currentMenu.equals("RemoveStock")) {
                    settings(false);
                } else if (currentMenu.equals("stockInfo")) {
                    chooseStock(false, "View", Choice.IMPLICIT, true);
                } else if (currentMenu.equals("WhatIfForm")) {
                    chooseStock(false, "WhatChoose", Choice.IMPLICIT,
                                false);
                } else if (currentMenu.equals("AlertForm")) {
                    chooseStock(false, "AddAlert", Choice.IMPLICIT,
                                false);
                } else if (currentMenu.equals("AnswerForm")) {
                    whatIfForm(stockSymbol);

                    // Screens off the Alerts menu
                } else if (currentMenu.equals("RemoveAlert") ||
                           currentMenu.equals("AddAlert")) {
                    alertMenu(false);
                } else if (currentMenu.equals("AddStock")) {
                    settings(false);
                }

                /*
                 * OK command executed, perform different actions
                 * depending on which screen is showing
                 */
            } else if (type.equals("Done")) {
                if (currentMenu.equals("AddStock")) {
                    if ((!stockSymbolBox.getString().trim().equals("")) &&
                        (addNewStock(stockSymbolBox.getString()))) {
                        settings(true);
                    }
                } else if (currentMenu.equals("AlertForm")) {
                    setAlert(((TextBox)currentDisplayable).getString());
                    alertMenu(true);

                    // Remove an alert
                } else if (currentMenu.equals("RemoveAlert")) {
                    boolean[] chosen = new boolean[choose.size()];
                    choose.getSelectedFlags(chosen);
                    for (int i = 0; i < chosen.length; i++) {
                        if (chosen[i]) {
                            removeAlert(choose.getString(i));
                        }
                    }
                    alertMenu(true);

                    // Remove a Stock
                } else if (currentMenu.equals("RemoveStock")) {
                    boolean[] chosen = new boolean[choose.size()];
                    choose.getSelectedFlags(chosen);
                    for (int i = 0; i < chosen.length; i++) {
                        if (chosen[i]) {
                            deleteStock(choose.getString(i));
                        }
                    }
                    chosen = null;
                    settings(true);

                    // Set the quote update interval
                } else if (currentMenu.equals("Updates")) {
                    switch (updatesChoices.getSelectedIndex()) {
                    case 0:
                        refresh_interval =    30000;
                        break;
                    case 1:
                        refresh_interval =   900000;
                        break;
                    case 2:
                        refresh_interval =  1800000;
                        break;
                    case 3:
                        refresh_interval =  3600000;
                        break;
                    case 4:
                        refresh_interval = 10800000;
                        break;
                    default:
                        break;
                    }
                    stockRefreshTask.cancel();
                    stockRefreshTask = new StockRefreshTask();
                    stockRefresh.schedule(stockRefreshTask, 0, 
                                          refresh_interval);
                    settings(true);
                }

                // Exit command executed
            } else if (type.equals("Exit")) {
                try {
                    destroyApp(true);
                }
                catch (MIDletStateChangeException msce) {
                    mainMenu();
                }

                // Calc command executed
            } else if (type.equals("Calc")) {
                if (origPurchPriceField.size() == 0) {
                    error("You must enter the price you originally "
                          + "purchased the stock at.", 2000);
                } else {
                    if (numSharesField.size() == 0) {
                        error("You must specify the number of shares"
                              +" to calculate with.", 2000);
                    } else {
                        calc();
                    }
                }

                // About command exectuted
            } else if (type.equals("About")) {
                about();

                /*
                 * No command button was pressed but a list selection
                 * was made
                 */
            } else {
                List shown = (List) display.getCurrent();

                /*
                 * if it's a menu not a list of stocks then we'll
                 * use a switch to select which action to perform
                 */
                if (currentMenu.equals("Main") ||
                    currentMenu.equals("Settings") ||
                    currentMenu.equals("AlertMenu")) {
                    switch (shown.getSelectedIndex()) {
                    case 0:

                        // View Stocks
                        if (currentMenu.equals("Main")) {
                            chooseStock(true, "View", Choice.IMPLICIT,
                                        true);

                            // Updates
                        } else if (currentMenu.equals("Settings")) {
                            updates();

                            // Add Alert
                        } else {
                            chooseStock(true, "AddAlert", Choice.IMPLICIT, 
                                        false);
                        }
                        break;
                    case 1:

                        // What If?
                        if (currentMenu.equals("Main")) {
                            chooseStock(true, "WhatChoose", 
                                        Choice.IMPLICIT, 
                                        false);

                            // Add Stock
                        } else if (currentMenu.equals("Settings")) {
                            addStock();

                            // Remove Alert
                        } else {
                            viewAlerts();
                        }
                        break;
                    case 2:

                        // Alerts
                        if (currentMenu.equals("Main")) {
                            alertMenu(false);

                            // Remove Stock
                        } else if (currentMenu.equals("Settings")) {
                            chooseStock(true, "RemoveStock", 
                                        Choice.MULTIPLE,
                                        false);
                        }
                        break;
                    case 3:

                        // Settings
                        if (currentMenu.equals("Main")) {
                            settings(false);
                        }
                        break;

                    default:
                        break;
                    }

                    /*
                     * we've now determined that it is a menu of stocks
                     * so we have to either show the stock info (from the
                     * View menu), add an alert (if Alert Choose screen is
                     * showing) or perform a What If? (if the What If?
                     * Choose screen is showing)
                     */
                } else {
                    if (currentMenu.equals("View")) {
                        displayStock(choose.getString(
                                     choose.getSelectedIndex())
                                     .substring(0, 
                                      choose.getString(choose
                                     .getSelectedIndex())
                                     .indexOf('@')-1));
                    } else if (currentMenu.equals("WhatChoose")) {
                        if (choose.getSelectedIndex() >= 0) {
                            whatIfForm(choose.getString(choose
                                                        .getSelectedIndex()));
                        }
                    } else if (currentMenu.equals("AddAlert")) {
                        if (choose.getSelectedIndex() >= 0) {
                            alertForm(choose.getString(choose
                                                       .getSelectedIndex()));
                        }
                    }
                }
            }

            synchronized (this) {
                // signal that another command can be processed
                commandThread = null;
            }
        }
    }

    /**
     * <p>This is an extension of the <code>TimerTask</code> class which runs
     * when called by <code>Timer</code>.  It refreshes the stock info for
     * each stock from the quote server and checks to see if any of the alerts
     * should be fired.</p>
     *
     * @see java.util.TimerTask
     */
    private class StockRefreshTask extends TimerTask {

        /**
         * <p>Execute the Timer's Task</p>
         */
        public void run() {
            try {

                // Just return if the database is empty
                if (stocks.getNumRecords() == 0) {
		    return; 
		}
		
                // Get all the records
                RecordEnumeration re = stocks.enumerateRecords();
                while (re.hasNextElement()) {
                    String tkrSymbol = 
			Stock.getName(new String(re.nextRecord()));
                    try {
                        byte[] rec = getStockQuote(tkrSymbol).getBytes();
			
                        // Update the record and check for any alerts that
			// may need to be executed
                        stocks.update(tkrSymbol, rec);
                        checkAlerts(tkrSymbol);
                    } catch (NumberFormatException nfe) {
                        error("\"" + tkrSymbol 
			      + "\" not found on server, or invalid data "
			      + "received from server", 2000);
                    }
		}   
	    } catch (Exception e) {
		error("Update Failed\n\nStocks were not updated", 2000);
	    }
	}
    }
}
